路遙知碼力,日久練成精- 只要在程式之路鑽研的夠深,便能夠充分發揮程式碼的力量; 練習的日子夠久,便能夠練成寫出精簡代碼的能力。
今天將會介紹python幾個常用的高階函數,
map(), filter(), reduce()。
高階函數?
我初聽到這個名稱的時候也覺得蠻新奇的,
玩遊戲可能會有高階魔法師、高階祭司、…等角色。
python的高階函數到底高階在哪裡呢?
其實高階函數就是把函數也當作參數傳遞進函數裡的函數。
相較於普通函數的定義方法,
這種寫法是比較高級的,
也增加了程式語言的靈活性。
看一個例子吧:
還記得在Day4第三個範例中,
我們曾教大家如何測量一段程式碼的執行時間嗎?
import time
tStart = time.time() #計時開始
# 這邊放入你想測量的測試碼
tEnd = time.time() #計時結束
print("Total time= %f seconds" % (tEnd - tStart))
在寫程式的時候,我們會想要知道自己的程式效率如何,
因此我們幫自己寫的不同函數測量執行時間,
但是如果要測量的函數很多,
就會寫很多重複的程式碼。
這時,我們就可以寫一個測量時間的函數來測量一個函數的執行的時間了。
程式碼如下:
import time
def measureTime(func, num):
tStart = time.time() #計時開始
for i in range(1000000):
func(num)
tEnd = time.time() #計時結束
print(f"{func.__name__} 總共執行了{(tEnd - tStart):.4f}秒")
參數func表示一個函數,num表示一個數字,
定義measureTime(func, num)為測量func(num)執行一百萬次的時間,
並把結果印出來,解說一下這行
print(f"{func.__name__} 總共執行了{(tEnd - tStart):.4f}秒")
func.__name__
可以取得函數的名稱,(tEnd - tStart):.4f
用來指定印出來的格式取到小數點後第四位數。
因為通常程式執行的速度非常快,
快到幾乎是一瞬間的事,
這邊我們選擇將func(num) 執行一百萬次測量時間,
若是你想要測量的函數本身執行時間已經很長,
也可依自身需求調整。
我們使用一個簡單的函數abs()做測試
measureTime(abs,-200)
結果: abs 總共執行了0.0625秒
(此結果可能因電腦效能而異,且結果不是固定數值)
明白了函數也可以當做參數傳遞後,
我們緊接著介紹什麼是生成器。
首先先介紹什麼是生成器,
Day12學過列表生成式,
例如要計算1的平方到5的平方存到列表中可以這樣寫:
L = [x*x for x in range(1,6)]
print(L)
結果: [1, 4, 9, 16, 25]
。
要創建一個生成器非常簡單,
只要把列表外面的[]
改成()
即可,例如:
g = (x*x for x in range(1,6))
print(g)
結果印出了<generator object <genexpr> at 0x000002917EA8F138>
那要怎麼樣可以印出生成器裡面的東西呢?
我們可以使用next()方法得到生成器的下一個元素:
g = (x*x for x in range(1,6))
print(next(g)) #印出 1
print(next(g)) #印出 4
print(next(g)) #印出 9
print(next(g)) #印出 16
print(next(g)) #印出 25
print(next(g)) #沒有元素了,出現StopIteration
但是我們比較少用next()方法取得下一個元素,
比較常見的方法為直接用for迴圈迭代:
例如:
g = (x*x for x in range(1,6))
for n in g:
print(n)
結果:1
4
9
16
25
我們也可以直接把生成器轉成列表:
g = (x*x for x in range(1,6))
print(list(g))
結果: [1, 4, 9, 16, 25]
看到這邊大家大概會有個疑問,
普通的列表與生成器都可以用for迴圈來迭代,
平時用起來感覺功能也差不多,
那到底我們要生成器幹嘛?
答案是省空間。
試想假設我們要創建一個含有一百萬個元素的列表,
但是可能實際會用到的元素只有前面幾個,
這時後面的空間都浪費掉了。
生成器保存的只是一種算法,
不會像列表生成式一樣,
要在一開始就把所有東西算好存起來,
而是邊循環邊計算,
節省大量的空間。
請看範例程式:
tStart = time.time() #計時開始
L = [x*x for x in range(1,10000000)]
print(sum(L))
tEnd = time.time() #計時結束
print(f"總共執行: {(tEnd - tStart):.4f}秒")
tStart = time.time() #計時開始
g = (x*x for x in range(1,10000000))
print(sum(g))
tEnd = time.time() #計時結束
print(f"總共執行: {(tEnd - tStart):.4f}秒")
這邊我們用列表生成式和生成器兩種不同的方法,
計算1的平方+2的平方+…+10000000的平方。
我的執行結果為:
333333283333335000000
總共執行: 0.9998秒
333333283333335000000
總共執行: 0.9529秒
由於列表生成式要事先把每個數字都算好存起來,
執行時間會比生成器略長一些。
講完了生成器,那什麼是迭代器呢?
生成器是迭代器的一種,
只是生成器特指以列表生成式語法加小括號產生的迭代器
凡是能透過next()方法取得下一個元素的都是迭代器。
(注意平時常見的list, str雖然可迭代,但不算迭代器)
迭代器的特性便是需要用到資料時才會去計算下一個元素,
以節省大量的空間。
如平時常見的range()函數也是一種迭代器。
注意因為迭代器是用到資料時才會去計算下一個元素,
迭代器本身沒有把元素事先存起來,
因此不能像列表一樣隨機存取(用index取值),
也不能進行切片運算。
例如下面程式 print(g[1])
是不合法的:
g = (x*x for x in range(1,6))
print(g[1])
會印出 'generator' object is not subscriptable
的錯誤訊息。
map(), filter(), reduce()都是能將函數當做參數的高階函數,介紹如下:
nums= [1,-2,-3]
ans = map(abs,nums)
print(list(ans))
結果為[1, 2, 3]
True
的項目組成一個 「迭代器」 返回。isEven
,def isEven(x):
return x % 2 == 0
我們再把利用filter過濾出1到6之間的偶數:
even_num = list(filter(isEven, [1, 2, 3, 4, 5, 6]))
print(even_num)
結果為: [2, 4, 6]
各位讀者看到這裡是否有種似曾相似的感覺了呢?
其實map() 和filter()的效果都是可以用列表生成式去做到的。
我們試著改寫上面的程式碼。
將列表的數字取絕對值
nums= [1,-2,-3]
ans = [abs(x) for x in nums]
print(ans)
結果為: [1, 2, 3]
取出列表的偶數
nums = [1, 2, 3, 4, 5, 6]
even_num = [x for x in nums if x % 2 == 0]
print(even_num)
結果為 [2, 4, 6]
例如: reduce(f, [x1, x2, x3, x4]) 代表的值為f(f(f(x1, x2), x3), x4)
。
reduce適合用在需要重複化簡列表內元素的情況,
我們看一個例子:
給你一個數字列表,
希望計算數字列表的乘積。
例如給定列表[4,5,6],
希望得到結果4*5*6 = 120。
我們看看reduce函數怎麼解,
要使用reduce函數,
需要引入內建模組functools
:
from functools import reduce
def prod(x,y):
return x*y
print(reduce(prod, [4,5,6]))
結果: 120
。
要計算數字列表的乘積,
我們可以先計算前兩個數的乘積,
把結果跟第三個數相乘,
一直做下去。
因此我們先定義一個函數prod()
,
計算兩個數相乘的結果,
再把prod函數傳進reduce的參數,
例如: reduce(prod, [x1, x2, x3, x4,…,xn]) 代表的值即為(((x1*x2)*x3)*x4*… *xn
。
匿名函數是不必定義函數名稱的定義函數方法,
匿名函數的基礎語法為:
lambda 參數: 返回值
匿名函數不必寫return,
直接返回值就是了。
例如:
sq = lambda x: x*x
其實就相當於你定義了這樣的一個函數:
def sq(x):
return x*x
通常匿名函數會與高階函數搭配使用,
例如上例filter 的例子:
利用filter過濾出1到6之間的偶數:
def isEven(x):
return x % 2 == 0
even_num = list(filter(isEven, [1, 2, 3, 4, 5, 6]))
print(even_num)
isEven
是功能非常簡單的函數,
特意去定義它還真有點麻煩,
這時很適合用匿名函數lambda
來改寫:
even_num = list(filter(lambda x: x%2==0, [1, 2, 3, 4, 5, 6]))
print(even_num)
類似的,範例14-3計算兩數乘積的函數功能簡單,
一樣用匿名函數定義即可:
from functools import reduce
print(reduce(lambda x, y : x*y, [4,5,6]))